package org.apache.click.doubleclick.annotationconfig;

import org.apache.click.Control;
import org.apache.click.Page;
import org.apache.click.doubleclick.annotationconfig.annotations.*;
import org.apache.click.service.ConsoleLogService;
import org.apache.click.service.LogService;
import org.apache.click.util.ClickUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;
import org.scannotation.archiveiterator.Filter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.servlet.ServletContext;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.*;

public class AnnotationConfigService extends ExtendedXmlConfigService {
    private AnnotationDB annotationDB;
    private boolean annotationEnabled = true;

    //change the access modifier when reading from click.xml is enabled.
    String annotationScanBasePackage;

    @Override
    public void onInit(ServletContext servletContext) throws Exception {
        Validate.notNull(servletContext, "Servlet context can not be null");
        annotationScanBasePackage = servletContext.getInitParameter("basePackage");

        annotationDB = scanBase(annotationScanBasePackage);

        // Set default logService early to log errors when services fail.
        logService = new ConsoleLogService();

        //saving servletContext
        this.servletContext = servletContext;

        InputStream inputStream = ClickUtils.getClickConfig(servletContext);

        try {
            Document document = ClickUtils.buildDocument(inputStream, this);

            Element rootElm = document.getDocumentElement();

            // Load the log service
            loadLogService(rootElm);
            if (annotationEnabled) {
                loadAnnotationLogService();
            }

            // Load the application mode and set the logger levels
            loadMode(rootElm);
            if (annotationEnabled) {
                loadAnnotationMode();
            }

            // Deploy click resources
            deployFiles(rootElm);
            if (annotationEnabled) {
                loadAnnotationControls(servletContext);
            }

            // Load the format class
            loadFormatClass(rootElm);

            // Load the common headers
            loadHeaders(rootElm);
            if (annotationEnabled) {
                Map<String, Object> appAnnotations = loadAppWideAnnotatedHeaders();
                appAnnotations.putAll(commonHeaders);
                commonHeaders = Collections.unmodifiableMap(appAnnotations);
            }

            // Load the pages
            loadPages(rootElm);

            // Load the error and not-found pages
            loadDefaultPages();

            // Load the charset
            loadCharset(rootElm);

            // Load the locale
            loadLocale(rootElm);

            // Load the File Upload service
            loadFileUploadService(rootElm);

            // Load the Templating service
            loadTemplateService(rootElm);

            // Load the Resource service
            loadResourceService(rootElm);

            // Load the PageInterceptors
            loadPageInterceptors(rootElm);

        } finally {
            ClickUtils.close(inputStream);
        }
    }

    protected void loadAnnotationLogService() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Set<String> stringLogServiceClasses = annotationDB.getAnnotationIndex().get(LoggingService.class.getName());
        if (stringLogServiceClasses == null || stringLogServiceClasses.size() == 0) {
            return;
        }
        Validate.isTrue(stringLogServiceClasses.size() == 1, "Found more then one log service. Only one log service is allowed");
        if (logService instanceof ConsoleLogService || logService == null) {
            Class<LogService> logServiceClass = checkIfDesiredType(stringLogServiceClasses.iterator().next(), LogService.class, LoggingService.class);
            logService = logServiceClass.newInstance();
        }
    }

    protected Map<String, Object> loadAppWideAnnotatedHeaders() throws ClassNotFoundException {
        Map<String, Object> annotatedHeaders = new HashMap<String, Object>();
        Set<String> stringHeaderClasses = annotationDB.getAnnotationIndex().get(Headers.class.getName());

        if (stringHeaderClasses != null && stringHeaderClasses.size() > 0) {
            for (String stringHeaderClass : stringHeaderClasses) {
                Class<Object> clazz = checkIfDesiredType(stringHeaderClass, null, null);
                if (!Page.class.isAssignableFrom(clazz)) {
                    Map<String, Object> tempAnnotationHeaders = loadAnnotationHeaders(clazz);
                    annotatedHeaders.putAll(tempAnnotationHeaders);
                }
            }
        }
        return annotatedHeaders;
    }

    //TODO: The parameter template files only contains templates from "/" and
    //TODO: "/WEB-INF/". We can enhance, so that user can provide package type
    //TODO: template location (ex: com.myapp.Hello.htm
    protected Map<String, Class<? extends Page>> loadAnnotationPageMap(List<String> templates) throws ClassNotFoundException {
        Map<String, Class<? extends Page>> annotatedPages = new HashMap<String, Class<? extends Page>>();

        Set<String> stringAnnotatedPages = annotationDB.getAnnotationIndex().get(Template.class.getName());

        if (stringAnnotatedPages == null || stringAnnotatedPages.size() == 0) {
            return annotatedPages;
        }

        for (String stringAnnotatedPage : stringAnnotatedPages) {
            Class<Page> pageClass = checkIfDesiredType(stringAnnotatedPage, Page.class, Template.class);
            Template templateAnnotation = pageClass.getAnnotation(Template.class);
            Validate.notEmpty(templateAnnotation.value(), "Page path is empty for page " + pageClass.getSimpleName());

            //TODO: we can enhance here to load pages from package level
            //TODO: read above TODO.
            if (!templates.contains(templateAnnotation.value())) {
                throw new RuntimeException("No template file found with name: " + templateAnnotation.value() + ", which " +
                        "is defined on " + pageClass.getSimpleName() + "'s @" + Template.class.getSimpleName() + " annotation");
            }
            annotatedPages.put(templateAnnotation.value(), pageClass);
        }
        return annotatedPages;
    }

    protected void loadAnnotationMode() throws ClassNotFoundException {
        Set<String> stringModes = annotationDB.getAnnotationIndex().get(ApplicationMode.class.getName());
        if (stringModes == null || stringModes.size() == 0) {
            return;
        }
        Validate.isTrue(stringModes.size() == 1, "Found more then one application mode. Only one mode is allowed");

        Class<?> clazz = checkIfDesiredType(stringModes.iterator().next(), null, null);
        ApplicationMode annotation = clazz.getAnnotation(ApplicationMode.class);

        //The default is DEVELOPMENT. So we change the value only if current value is DEVELOPMENT but
        // annotation value is different. Read the tutorial.
        if ((mode == Mode.DEVELOPMENT.getMode()) && (mode != annotation.value().getMode())) {
            mode = annotation.value().getMode();
            setLogLevel();
        }
    }

    protected void loadAnnotationAutoBinding() throws ClassNotFoundException {
        Set<String> stringAutoBindings = annotationDB.getAnnotationIndex().get(org.apache.click.doubleclick.annotationconfig.annotations.AutoBinding.class.getName());
        if (stringAutoBindings == null || stringAutoBindings.size() == 0) {
            //if manually not set, and no annnotation found, then set to default
            if (autobinding == null) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.DEFAULT;
            }
            return;
        }

        Validate.isTrue(stringAutoBindings.size() == 1, "Found more then one auto binding mode annotation. ONly one auto binding mode is allowed.");
        Class<?> clazz = checkIfDesiredType(stringAutoBindings.iterator().next(), null, null);
        org.apache.click.doubleclick.annotationconfig.annotations.AutoBinding annotation = clazz.getAnnotation(org.apache.click.doubleclick.annotationconfig.annotations.AutoBinding.class);
        autobinding = annotation.value();
    }

    protected void loadAnnotationControls(ServletContext servletContext) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Set<String> stringControlClasses = annotationDB.getAnnotationIndex().get(DeployAwareControl.class.getName());
        if (stringControlClasses == null || stringControlClasses.size() == 0) {
            return;
        }
        for (String stringControlClass : stringControlClasses) {
            Class<Control> controlClass = checkIfDesiredType(stringControlClass, Control.class, DeployAwareControl.class);
            DeployAwareControl annotation = controlClass.getAnnotation(DeployAwareControl.class);
            boolean isOnDeploy = annotation.value();

            if (isOnDeploy) {
                controlClass.newInstance().onDeploy(servletContext);
            }
        }
    }

    private <T> Class<T> checkIfDesiredType(String stringClass, Class<T> desiredType, Class<? extends Annotation> availableAnnotation) throws ClassNotFoundException {
        Class<?> clazz = ClickUtils.classForName(stringClass);

        //if desired type is null, then just return the class. Ex. @ApplicationMode does not require to check if its assignable form.
        //but still we need the class file to process the annotation.
        if (desiredType != null && !desiredType.isAssignableFrom(clazz)) {
            throw new RuntimeException(clazz.getSimpleName() + " is annotated with @" + availableAnnotation.getSimpleName() + ", but does not implement " + desiredType.getSimpleName());
        }
        return (Class<T>) clazz;
    }

    protected AnnotationDB scanBase(String stringBasePackage) throws IOException {
        Validate.notEmpty(stringBasePackage, "Base package can not be null or empty");
        String tempBasePackage = stringBasePackage.replace(".", "/");

        URL base = ClasspathUrlFinder.findResourceBase(tempBasePackage);
        Filter filter = new CustomFilter(stringBasePackage);
        annotationDB = new ExtendedAnnotationDB(filter);
        annotationDB.setScanFieldAnnotations(false);
        annotationDB.setScanMethodAnnotations(false);
        annotationDB.setScanParameterAnnotations(false);

        annotationDB.scanArchives(base);
        return annotationDB;
    }

    protected void loadPages(Element rootElm) throws ClassNotFoundException {
        List pagesList = ClickUtils.getChildren(rootElm, "pages");

        //NOTE: commented out, because this was necessary for click.xml
        //NOTE: but not anymore for annotation.

//        if (pagesList.isEmpty()) {
//            String msg = "required configuration 'pages' element missing.";
//            throw new RuntimeException(msg);
//        }

        List<String> templates = getTemplateFiles();

        //load manual page mappings for all packages
        if (pagesList != null && pagesList.size() > 0) {
            for (int i = 0; i < pagesList.size(); i++) {
                Element pagesElm = (Element) pagesList.get(i);

                // Determine whether to use autobinding.
                determineAutoBinding(pagesElm);

                String pagesPackage = pagesElm.getAttribute("package");
                if (StringUtils.isBlank(pagesPackage)) {
                    pagesPackage = "";
                }

                pagesPackage = pagesPackage.trim();
                if (pagesPackage.endsWith(".")) {
                    pagesPackage = pagesPackage.substring(0, pagesPackage.length() - 2);
                }

                // Add the pages package to the list of page packages
                pagePackages.add(pagesPackage);

                //building manual page mapping
                buildManualPageMapping(pagesElm, pagesPackage);
            }
        }

        //loading annotated page mappings
        Map<String, Class<? extends Page>> annotatedPages = loadAnnotationPageMap(templates);
        if (logService.isDebugEnabled()) {
            logService.debug("annotated pages:");
        }

        //loading annotation auto binding configuration
        loadAnnotationAutoBinding();

        if (annotatedPages != null || annotatedPages.size() > 0) {
            for (String annotatedPagePath : annotatedPages.keySet()) {
                if (!pageByPathMap.containsKey(annotatedPagePath)) {

                    ExtendedXmlConfigService.PageElm page = new ExtendedXmlConfigService.PageElm(
                            annotatedPagePath,
                            annotatedPages.get(annotatedPagePath),
                            commonHeaders,
                            autobinding);

                    pageByPathMap.put(page.getPath(), page);

                    if (logService.isDebugEnabled()) {
                        String msg = page.getPath() + " -> " + page.getPageClass().getName();
                        logService.debug(msg);
                    }
                }
            }
        }

        //loading automap pages
        for (int i = 0; i < pagesList.size(); i++) {
            Element pagesElm = (Element) pagesList.get(i);

            // Determine whether to use automapping
            boolean automap = determineAutoMap(pagesElm);

            String pagesPackage = pagesElm.getAttribute("package");
            if (StringUtils.isBlank(pagesPackage)) {
                pagesPackage = "";
            }

            pagesPackage = pagesPackage.trim();
            if (pagesPackage.endsWith(".")) {
                pagesPackage = pagesPackage.substring(0, pagesPackage.length() - 2);
            }

            // Add the pages package to the list of page packages
            pagePackages.add(pagesPackage);

            //building manual page mapping
            buildManualPageMapping(pagesElm, pagesPackage);

            if (automap) {
                buildAutoPageMapping(pagesElm, pagesPackage, templates);
            }
        }

        buildClassMap();
    }

    private boolean determineAutoMap(Element pagesElm) {
        boolean automap = true;
        String automapStr = pagesElm.getAttribute("automapping");
        if (StringUtils.isBlank(automapStr)) {
            automapStr = "true";
        }

        if ("true".equalsIgnoreCase(automapStr)) {
            automap = true;
        } else if ("false".equalsIgnoreCase(automapStr)) {
            automap = false;
        } else {
            String msg = "Invalid pages automapping attribute: " + automapStr;
            throw new RuntimeException(msg);
        }
        return automap;
    }

    private void determineAutoBinding(Element pagesElm) {
        String autoBindingStr = pagesElm.getAttribute("autobinding");
        if (StringUtils.isBlank(autoBindingStr)) {
            autobinding = org.apache.click.service.ConfigService.AutoBinding.DEFAULT;
        } else {

            if ("annotation".equalsIgnoreCase(autoBindingStr)) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.ANNOTATION;

            } else if ("public".equalsIgnoreCase(autoBindingStr)) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.DEFAULT;

            } else if ("none".equalsIgnoreCase(autoBindingStr)) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.NONE;

                // Provided for backward compatibility
            } else if ("true".equalsIgnoreCase(autoBindingStr)) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.DEFAULT;

                // Provided for backward compatibility
            } else if ("false".equalsIgnoreCase(autoBindingStr)) {
                autobinding = org.apache.click.service.ConfigService.AutoBinding.NONE;

            } else {
                String msg = "Invalid pages autobinding attribute: "
                        + autoBindingStr;
                throw new RuntimeException(msg);
            }
        }

        // TODO: if autobinding is set to false an there are multiple pages how should this be handled
        // Perhaps autobinding should be moved to <click-app> and be a application wide setting?
        // However the way its implemented above is probably fine for backward compatibility
        // purposes, meaning the last defined autobinding wins

    }
}
